iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 8
0

嗨大家今天過得好嗎?週末難得有空閒可以走出舒適圈多認識新朋友,便下載了交友軟體開始左滑、右滑起來,突然想到這一頁接一頁的換頁之流暢,換頁之間又帶點動畫特效就像真的翻頁,難道是用 ViewPager 實現的?聽說還新出了繼承自 RecyclerView 的 ViewPager2 也來研究一下好了!( 憑實力單身 )

實作方式

跟 RecyclerView 要搭配 RecyclerView.Adapter 一樣,ViewPager 也有搭配的 PagerAdapter,PagerAdapter 有兩個必要實作的 method 是override fun isViewFromObject(view: View, object: Any): Booleanoverride fun getCount(): IntisViewFromObject 是要回傳當前畫面和綁定的物件是否一致,getCount 是返回當前畫面的個數,但實作這兩個物件後只是讓 PagerAdapter 可以處理換頁的邏輯,要把物件和畫面綁定的方法是透過 override fun instantiateItem(container: ViewGroup, position: Int): Any 的 method,通常會在 instantiateItem 的 method 中創建子畫面、綁定到 container 上、最後再回傳子畫面 ( isViewFromObject 就是比對當前的畫面和 instantiateItem 的子畫面是否相符 )。有綁定畫面和物件的方法當然還有移除的方法是 override fun destroyItem(container: ViewGroup, position: Int, view: Any),把 container 階層中的 view 移除。

透過 PagerAdapter 的實作我們已經可以實現一個換頁的機制了,也有基本的換頁動畫,但如果想來點不一樣的動畫,譬如縮放顯示或淡入淡出的話要怎麼實現呢?可以透過 ViewPaager.PageTransformer 處理!在 PageTransformer 的 transformPage() method 會提供 ViewPager 當前頁面的 position 還有當前的 View,position 的數值介於 -1 到 1 之間,代表向後或是向前滑動,停在當前的位置就代表 position 為 0,利用 position 可以對 View 的縮放、透明度做比例特效。交友軟體的左滑右滑應該也是透過 position 這個參數實現動畫的。

自動換頁 + 無限換頁

自動換頁的效果可以透過 Handler 的方式指定 ViewPager 的當前頁面, setCurrentItem(position, true)true 的話就會有換頁動畫,看起來就像是自動滑動 ViewPager 一樣,如果不想要動畫效果就改成 false 就好。使用 Handler 記得要在 View 銷毀時一併移除 callback,不然就可能會有 Leak 的問題。

那如果滑動到最後一頁時想要接續滑動到第一頁,達到像無限換頁的效果呢?首先需要讓 PagerAdapter 的數量前後加一頁,另外再新增 method 維護正確的 position,再來使用 addOnPageChangeListener 監聽滑動,在 listener 的 onPageSelected 紀錄當前的頁面和設定下一頁要換到哪一頁,當從最後兩頁滑到最後一頁時,在滑動動畫結束時載入第一頁作為下一頁,看起來就像無限滑動的列表,同理從第一頁往前滑時也是會滑到最後一頁,在滑動結束時載入倒數第二頁。

話不多說直接上 code:

class BannerPagerAdapter(val listener: BannerPagerAdapterListener) : PagerAdapter() {

    val list = mutableListOf<Banner>()

    interface BannerPagerAdapterListener {
        fun openBanner(banner: Banner)
    }

    override fun isViewFromObject(view: View, any: Any): Boolean = view == any

    override fun getCount(): Int = if (list.isEmpty()) 0 else list.size + 2

    override fun instantiateItem(container: ViewGroup, position: Int): Any {
        val bannerView = LayoutInflater.from(container.context).inflate(R.layout.adapter_banner_pager, container, false)
        val bannerCover = bannerView.venue_activities_banner_pager_item_cover

        val virtualPosition = getRealPosition(position)
        
        bannerView.setOnClickListener {
            if (list.isNotEmpty()) {
                listener.openVenueActivity(list[virtualPosition])
            }
        }
        container.addView(bannerView)
        return bannerView

    }

    override fun destroyItem(container: ViewGroup, position: Int, view: Any) {
        container.removeView(view as View)
    }

    private fun getRealPosition(position: Int): Int {
        return if (position == 0) {
            list.size - 1
        } else if (position == list.size + 1) {
            0
        } else {
            position - 1
        }
    }

    fun addAll(banners: List<Banner>) {
        if (banners.isNotEmpty()) {
            this.list.clear()
            this.list.addAll(venueActivity)
            notifyDataSetChanged()
        }
    }
}

class MainFragment() {

	private var handler: Handler? = null
	private var bannerViewPager: ViewPager? = null

	override fun onViewCreated() {

		bannerViewPager = view.findViewById<View>(R.id.banner_view_pager) as ViewPager
		handler = Handler()    // on UI thread
    val autoPager = Runnable {
        bannerViewPager?.let {
            if (it.currentItem + 1 < it.adapter?.count ?: 0) {
                it.currentItem += 1
            }
        }
    // 因為前後都有加一頁,因此第一頁的 index 是 1 不是 0
    handler?.postDelayed({ bannerViewPager?.setCurrentItem(1, false) }, INITIAL_DELAY.toLong())
    handler?.postDelayed(autoPager, BANNER_SWITCH_DELAY.toLong())

    bannerViewPager?.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
        var jumpPosition = -1
        override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
        override fun onPageSelected(position: Int) {
            val realCount = bannerViewPager?.adapter?.count?.minus(2) ?: 0
            if (position == 0) {
                jumpPosition = realCount
            } else if (position == realCount + 1) {
                jumpPosition = 1
            }
        }

        override fun onPageScrollStateChanged(state: Int) {
            handler?.removeCallbacksAndMessages(null) // If token is null, all callbacks and messages will be removed.
            if (state == ViewPager.SCROLL_STATE_IDLE) {
                bannerIsDragging(false)
                // 倒數最後一頁和第一頁在動畫結束時分別載入第一頁和最後一頁
                if (jumpPosition >= 0) {
                    bannerViewPager?.setCurrentItem(jumpPosition, false)
                    jumpPosition = -1
                }
                handler?.postDelayed(autoPager, BANNER_SWITCH_DELAY.toLong())
            } else if (state == ViewPager.SCROLL_STATE_DRAGGING) {
                bannerIsDragging(true)
            }
        }
    })
}

明天會繼續分享 Pager 的主題,討論 ViewPager2 有哪些改變還有怎麼轉移,喜歡今天分享內容的邦友請繼續關注「打造一個厲害的普通 Android App - 使用者體驗優化」的主題,我們明天見。


上一篇
眼前的黑不是黑是 Dark theme
下一篇
ViewPager (2):ViewPager2
系列文
打造一個厲害的普通 Android App - 使用者體驗優化16
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言